HashMap详解

目录

1.简介

2.数据结构及实现

2.1 jdk1.7的关键代码实现

2.2 关于hash槽位-jdk8

3.深入理解

3.1 为什么JDK1.8 HashMap链表使用尾插法,而不继续使用头插法?

3.2 JDK1.8如何扩容的?

3.3 为什么扩容后的新位置落点等于原位置加上原容量?


1.简介

         HashMap是map(键值对,jdk1.2开始替换Dictionary)接口的实现类,允许空值、同时不保证有序。

 * Hash table based implementation of the <tt>Map</tt> interface.  This
 * implementation provides all of the optional map operations, and permits
 * <tt>null</tt> values and the <tt>null</tt> key.  (The <tt>HashMap</tt>
 * class is roughly equivalent to <tt>Hashtable</tt>, except that it is
 * unsynchronized and permits nulls.)  This class makes no guarantees as to
 * the order of the map; in particular, it does not guarantee that the order
 * will remain constant over time.

2.数据结构及实现

        数据结构JDK7为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现。链表是为了解决哈希冲突,红黑树是为了解决链表太长导致的查询速度变慢。

        hash计算公式为:(h = key.hashCode()) ^ (h >>> 16)。为什么要这样做呢?因为hashCode用int表达,4个字节,有32位,hash与内部数组长度进行与运算计算位置时,大量的高位数据未参与计算,会导致与运算后的槽位冲突增大,因此需要做一些运算将hash值的高位数据与低位数据进行混合,增加hash值低位数据的复杂度,降低槽位(数据存放位置)冲突概率。

2.1 jdk1.7的关键代码实现

          初始化大小2*4次方,最大容量2*30次方,默认扩容系数0.75。

          存储数据主要使用Entry<K,V>数组,数组中的每个Entry<K,V>是个链表结构。

        hashmap获取数据比较简单

        hashmap存入数据

        看看putForNullKey,遍历Entry<K,V>数组第一个元素的所有链表,如果链表中有key也是null的数据,则进行value替换,同时返回旧value值;如果没有,把它放进到hash槽第一个位置(即数组的第一个位置),也处于链表中的第一位(注意hashmap中的链表类似于栈的结构,先增加的元素总是在最前面,后面连接着一群Entry<K,V>,如果后面没有元素,下一个元素就连接着null,这种存储方式同时也解决了Hash冲突问题)。

        后续如果key不为null,则开始根据key计算hash值;在不扩容的情况下,Entry<K,V>数组的大小是固定的,这个key-value值到底要放在哪,需要通过indexFor计算出来(简称槽位),后续遍历Entry<K,V>数组的所在槽位[i]的所有链表数据(table[i]槽位如果有数据,则存放着一条链表),如果链表中某个Entry<K,V>中的key等于待放入数据的key,则替换原value值为新value值,同时返回旧value值;如果槽位i位置没有该key对应的Entry<K,V>数据,则将该key-value放到该槽位的第一个位置,后续连接其它数据或null。

2.2 关于hash槽位-jdk8

          tableSizeFor为计算hashmap容量的关键代码,旨在获取输入容量cap的最大2次方数,实现比较巧妙,利用计算机底层二进制位移、或运算的特点,将cap-1的二进制低位全部变为1,相当于把能包含cap-1这个数的二进制位置填充满,后面再加1,进行进位(高位进一位,低位全部变为0),最后形成能包含cap-1的最大2次方数。

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

3.深入理解

3.1 为什么JDK1.8 HashMap链表使用尾插法,而不继续使用头插法?

        先看JDK1.7的头插法:当hash冲突时,链表中新增加的值存放于旧值前。如下图,冲突时先获取bucketIndex处的旧Entry,然后构建新Entry,然后将旧Entry挂在新Entry的后面(next=n)。

         再看看JDK1.7扩容时也是使用的头插法:先将旧e的next指向新table的entry(第一次为null),再newTable[i] = e,旧e再占据newTable的i位置,也就是说旧元素转移到新表后,始终占据在某位置的头部位置。

         然后说说扩容时出现的循环情况:旧表某位置的链表挂载2个元素,a->b->null,扩容重新hash后,它们又落在新表的同一个位置,这是先转移a元素到新位置,a->null,再转移第二个元素b,执行e.next = newTable[i],在新表中b->a->null,而在旧表中a->b,这个连接还没断开,就会造成a、b之间形成循环,扩容将一直卡在a、b转移过程中;所有JDK1.8改变为尾插法解决这个问题。

        如下图红线部分,当putVal出现hash冲突时,循环遍历到链表的尾部,(e = p.next) == null,然后构建新Node,将新Node挂在p的后面,成为新的尾部,这就是尾插法;使用尾插法,每次都往后面勾连数据,扩容转移也就不会产生循环了,不明白的可以模拟下。

        

3.2 JDK1.8如何扩容的?

        JDK1.8扩容关键代码如下,扩容思想:与JDK1.7不一样,未使用新容量大小与hash作与运算确定新位置,而是在定死每次扩容1倍(即乘2)的基础上,利用二进制的特性(e.hash &oldCap == 0,后面分析),确定哪些节点落在扩容后的新位置上,哪些落在原位置上,在挪动的时候,使用的也是后插法(loTail.next = e、hiTail.next = e)。

         分析下e.hash &oldCap == 0的使用,如果该等式成立,则表明节点落在原位置,否则为新位置(2倍,2选1)。

        为什么会这样呢?那是因为位置计算公式为e.hash&(cap-1),而cap都是2的次方,cap-1用二进制表示(假设cap为4,2^2=4,计从右往左数第2+1个位置为cap位)为0011,由于0与任何数进行与运算都为0,实际参与运算的只有后2个1(即除cap位置外的所有低位),同理新位置计算为2cap-1,2*cap无非就是二进制高位多进了一位(0111),为了使多进的这一个1不影响位置运算(与原位置一样),我们得使它运算为0。

        怎样使它为0呢,那就是我们的hash在多进的这一位(即cap位置)始终为0,怎么判断hash在cap位置为0呢,拿它与在cap位置为1的数(0100,就是cap)进行与运算,如果等于0,那就表明节点在这个位置上为0;举个例子,hash值二进制为1011 0010,它在cap位置为0(从右往左数第2+1个),它与cap-1(0011)进行与运算,结果为0000 0010,与2cap-1(0111)运算,结果也为0000 0010,位置落点一样。

3.3 为什么扩容后的新位置落点等于原位置加上原容量?

        newTab[j + oldCap] = hiHead,这段代码表明扩容后,hash移动的新位置为原位置+oldCap;在理解了上面部分说明后,其实很好理解,e.hash &oldCap == 0等式不成立,说明cap高位为1,那它与cap高位(从右往左数第2+1个位置)为0的区别,就是它比原位置大了2^2,即cap,所以新位置为原位置+oldCap。

        简单说明下loTail、loHead、hiTail、hiHead的意义,这4个变量分别表明低位区尾部、低位区头部、高位区尾部、高位区头部,低位区即原位置,高位区为新位置,链表数据都从头往尾勾连,尾部变量主要起到缓冲转换作用,等链表数据全部分类好后,直接把头部数据挂在对应位置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kenick

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值